Skip to content

Implementation of matplotlib backend for criterion_plot() #599

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

r3kste
Copy link
Contributor

@r3kste r3kste commented May 19, 2025

Summary of Changes

  1. Added backend parameter to criterion_plot() which accepts "plotly" or "matplotlib"
  2. criterion_plot() now returns the figure object corresponding to the chosen backend.
  3. Added backends.py which serves as a registry for all backends

To-Do

It is very much still a work in progress.

  • Discuss on whether the dispatch mechanism can be improved.
  • Add examples in documentation
  • Add suitable tests

Plot

Plotly Matplotlib
t t

Copy link

codecov bot commented May 19, 2025

Codecov Report

❌ Patch coverage is 87.36842% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/optimagic/visualization/backends.py 81.53% 12 Missing ⚠️
Files with missing lines Coverage Δ
src/optimagic/visualization/history_plots.py 93.64% <100.00%> (+1.08%) ⬆️
src/optimagic/visualization/plotting_utilities.py 95.54% <100.00%> (+0.50%) ⬆️
src/optimagic/visualization/backends.py 81.53% <81.53%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@r3kste r3kste force-pushed the backend_plotting branch from 3efa340 to f780bf9 Compare May 19, 2025 19:11
@r3kste r3kste changed the title Barebones implementation of matplotlib backend for criterion_plot() Implementation of matplotlib backend for criterion_plot() May 20, 2025
@r3kste r3kste force-pushed the backend_plotting branch 4 times, most recently from 556334a to d0c6856 Compare May 25, 2025 18:35
@r3kste r3kste closed this Jul 31, 2025
@r3kste r3kste force-pushed the backend_plotting branch from d0c6856 to 1f95f25 Compare July 31, 2025 16:43
@r3kste r3kste reopened this Jul 31, 2025
@r3kste r3kste force-pushed the backend_plotting branch from 97d9169 to e187294 Compare July 31, 2025 16:50
Copy link
Member

@timmens timmens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot! I have a few comments, if you have any questions let me know here or via Zulip. I think it will be important to get this PR right such that we don't complicate our lives when handling the other visualization functions.

My main concern is that I'd like the usage of the plotting class to feel more natural. See one of my comments in the backends module.

On an abstract level, a plotting class supporting the criterion plot needs to be able to:

  • Create a figure
  • Add lines to that figure
  • Set a template
  • Set a legend position
  • Set axis labels

Now I would expect that our plotting class somehow incorporates / supports these actions or an action that combines a subset of them (e.g., if we think that some of these actions comprise one block, like we currently do with the PlotConfig).

Additionally

  1. As I am saying in one of the comments, we need to check whether we actually need the PlotConfig class.
  2. I thought about your proposal to rename OptimizeResultOrPath to ResultOrPath, and I think I prefer that! Could you change that?

Thanks a lot!!

Comment on lines +32 to +50
class BackendRegistry:
_registry: dict[str, type[BackendWrapper]] = {}

@classmethod
def register(cls, backend_name: str) -> Callable:
def decorator(backend_wrapper):
cls._registry[backend_name] = backend_wrapper
return backend_wrapper

return decorator

@classmethod
def get_backend_wrapper(cls, backend_name: str) -> type[BackendWrapper]:
if backend_name not in cls._registry:
raise ValueError(
f"Backend '{backend_name}' is not supported. "
f"Supported backends are: {', '.join(cls._registry.keys())}."
)
return cls._registry[backend_name]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this BackendRegistry is pretty cool I think we can replace it by a registry dictionary and a function to reduce complexity. Let me know what you think about the trade-offs here.

def get_plotting_backend_class(backend: str) -> PlottingWrapper:
    backend_to_class = {
        "plotly": PlotlyBackend,
        "matplotlib": MatplotlibBackend,
    }
    if backend not in backend_to_class:
        msg = (
            f"Backend '{backend}' is not supported. Supported backends are: "
            f"{', '.join(backend_to_class.keys())}."
        )
        raise ValueError(msg)

    return backend_to_class[backend]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

def get_plotting_backend_class(backend: str) -> PlottingWrapper:
    backend_to_class = {
        "plotly": PlotlyBackend,
        "matplotlib": MatplotlibBackend,
    }

Is there any specific reason why the dictionary should be defined within the function rather than as a global object?

get_palette_cycle,
)

_criterion_plot_legend: dict[str, dict[str, Any]] = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_criterion_plot_legend: dict[str, dict[str, Any]] = {
CRITERION_PLOT_BACKEND_TO_LEGEND: dict[str, dict[str, Any]] = {

Comment on lines +12 to +29
class BackendWrapper(abc.ABC):
default_template: str
default_palette: list

def __init__(self, plot_config: PlotConfig):
self.plot_config = plot_config

@abc.abstractmethod
def line_plot(self, lines: list[LineData]) -> None:
pass

@abc.abstractmethod
def label(self, **kwargs):
pass

@abc.abstractmethod
def return_obj(self):
pass
Copy link
Member

@timmens timmens Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not entirely convinced by the methods and their names of this class.

  1. line_plot is not returning a plot, but adding lines to a figure that is created in the init. It should probably be called add_lines?
  2. The label method also updated the config in the plotly class and the legend in the matplotlib class. I think it should be called set_labels and only update the labels.
  3. To update the legend we should probably add a set_legend_position method.
  4. Where do we need to set the template? Can we set the template in the init?
  5. Why do we need a PlotConfig object now? Could we do without it?

I think in particular I would like the usage of the class to be easier and more natural. Something in the direction of:

    plot = plot_cls(template)

    plot.add_lines(lines + multistart_lines)
    plot.set_labels(
        x_label="No. of criterion evaluations",
        y_label="Criterion value"
    )
    plot.set_legend_position(legend=CRITERION_PLOT_BACKEND_TO_LEGEND[backend])

    return plot.backend_native_figure

Copy link
Contributor Author

@r3kste r3kste Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To update the legend we should probably add a set_legend_position method.

I agree, although I think set_legend_props might be a better fit as it can also be used for any additional properties specific to the legend.

Where do we need to set the template? Can we set the template in the init?

  1. plotly requires it to be after creating the figure and
  2. matplotlib requires it before creating the figure.

As we are creating the figure within the init, we can also set template in init.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants